from django.db import models
from .material import MaterialCategory, MaterialTag
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
from django.contrib.postgres.fields import JSONField, ArrayField
from .template import Template
import mongoengine as me
from bson.objectid import ObjectId
from mongoengine.errors import ValidationError, DoesNotExist
from django.utils import timezone
from django.utils.translation import gettext as _, ugettext
import re
import logging
from enum import Enum, IntEnum

from apps.accounts.models.users import User
from .file import UploadedSourceFile

# from apps.search.utils.serilizers.json import JSONHandler
# from apps.search.utils.serilizers.common import DataMode

'''
lyp:我还是把DataMetaFieldEnum类加了进来，因为没有这个对后面的搜索功能来说很不方便
'''
_KEYWORDS_SEP = re.compile(r'[,，]')


class DataMetaFieldEnum(Enum):
    """
    DataMeta字段
    """
    DATA_ID = 'Data ID'
    TITLE = "Title"
    DOI = "DOI"
    ABSTRACT = "Abstract"
    MATERIAL_CLASS = "Material Class"
    TEMPLATE_NAME = "Template Name"
    KEYWORDS = "Keywords"
    AUTHOR = "Author"
    CREATED_AT = "Created at"
    SOURCE = "Source"
    REFERENCE = "Reference"
    OTHER_INFO = "Other Info"
    TEMPLATE_ID = "Template ID"

    VIEWS = "views"
    DOWNLOADS = "downloads"
    SCORE = "score"
    CATEGORY = "category"

    @property
    def sql_name(self):
        if self == DataMetaFieldEnum.CREATED_AT:
            return 'add_time'
        elif self == DataMetaFieldEnum.DATA_ID:
            return 'id'
        elif self == DataMetaFieldEnum.TEMPLATE_ID:
            return 'tid'
        elif self == DataMetaFieldEnum.AUTHOR:
            return 'user__real_name'
        elif self == DataMetaFieldEnum.CATEGORY:
            from django.utils.translation import get_language
            if 'zh-hans' == get_language():
                return "category__name_zh"
            return "category__name_en"
        else:
            return self.name.lower()

    @property
    def description(self):
        return ugettext(self.value)

    @property
    def required(self):
        if self in (
                DataMetaFieldEnum.TITLE,
        ):
            return True
        else:
            return False

    def get_value(self, data_meta):
        """
        从DataMeta对象中获取字段的值
        :param data_meta:
        :return:
        """
        if self == self.__class__.DATA_ID:
            return data_meta.id
        elif self == self.__class__.TITLE:
            return data_meta.title
        elif self == self.__class__.MATERIAL_CLASS:
            return data_meta.category.name
        elif self == self.__class__.TEMPLATE_ID:
            return str(data_meta.template_id)
        elif self == self.__class__.KEYWORDS:
            return ', '.join(data_meta.keywords or [])
        elif self == self.__class__.AUTHOR:
            return data_meta.user_real_name
        elif self == self.__class__.CREATED_AT:
            return data_meta.add_time.strftime('%Y-%m-%d %H:%M:%S')
        elif self == self.__class__.SOURCE:
            return data_meta.source
        elif self == self.__class__.REFERENCE:
            return data_meta.reference
        elif self == self.__class__.OTHER_INFO:
            return '; '.join([f'{key}: {value}' for key, value in (data_meta.other_info or {}).items()])
        elif self == self.__class__.DOI:
            return data_meta.doi
        elif self == self.__class__.ABSTRACT:
            return data_meta.abstract

    def get_value_from_dict(self, meta_dict: dict, translated_key=False):
        if translated_key:
            return self.validate(meta_dict.get(self.description))
        else:
            return self.validate(meta_dict.get(self.field_raw_name))

    def validate(self, value):
        if value is None:
            if self.required:
                from apps.storage.utils.serializers.common import ParsingError, ParsingErrorEnum
                raise ParsingError(ParsingErrorEnum.FIELD_MISSING, o1=self.description)
            else:
                return None
        if self == DataMetaFieldEnum.KEYWORDS:
            if not isinstance(value, list):
                if not isinstance(value, str):
                    from apps.storage.utils.serializers.common import ParsingError, ParsingErrorEnum
                    raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=type(value).__name__, o2=self.description)
                return [x.strip() for x in value.split(',')]
            return [str(x) for x in value]
        elif self == DataMetaFieldEnum.OTHER_INFO:
            pass  # TODO
        return str(value)

    @property
    def field_raw_name(self):
        """
        获取DataMeta JSON中的字段名
        :return:
        """
        return self.name.lower()

    @staticmethod
    def writer_iterator(with_id=False):
        """
        导出Excel时DataMeta页填写的字段
        :return: 迭代器
        """
        _meta_fields = DataMetaFieldEnum.writer_meta_fields()
        if with_id:
            _meta_fields = ["Data ID"] + _meta_fields
        index = 0
        while index < len(_meta_fields):
            yield DataMetaFieldEnum(_meta_fields[index])
            index += 1

    @staticmethod
    def reader_iterator(with_id=False):
        """
        生成导入模板时DataMeta页填写的字段
        :return: 迭代器
        """
        _meta_fields = DataMetaFieldEnum.reader_meta_fields()
        if with_id:
            _meta_fields = [DataMetaFieldEnum.DATA_ID.value] + _meta_fields
        index = 0
        while index < len(_meta_fields):
            yield DataMetaFieldEnum(_meta_fields[index])
            index += 1

    @staticmethod
    def reader_meta_fields():
        return ["Title", "DOI", "Abstract", "Keywords", "Source", "Reference"]

    @staticmethod
    def writer_meta_fields():
        return ["Title", "Material Class", "DOI", "Abstract", "Keywords", "Author", "Created at",
                "Source", "Reference", "Other Info"]


class DataMetaManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(importing=False)


class ImportingDataMetaManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(importing=True)


class DataMeta(models.Model):
    title = models.CharField(max_length=255)
    category = models.ForeignKey(MaterialCategory, null=True,
                                 on_delete=models.SET_NULL, limit_choices_to={'leaf': True})
    source = JSONField()
    tid = models.IntegerField()
    keywords = ArrayField(models.CharField(max_length=32), null=True, blank=True, default=list)
    doi = models.CharField(max_length=255, null=True, blank=True)
    abstract = models.CharField(max_length=255, null=True, blank=True)
    purpose = models.CharField(max_length=255, null=True, blank=True)
    author = models.ForeignKey(to='accounts.User', null=True, on_delete=models.SET_NULL)
    # author = models.CharField(max_length=32, default='')
    add_time = models.DateTimeField(default=timezone.now)
    downloads = models.IntegerField(default=0)
    views = models.IntegerField(default=0)
    score = models.IntegerField(default=0, validators=(MinValueValidator(0), MaxValueValidator(10)))
    dc_id = models.CharField(max_length=24, validators=(RegexValidator(r'^[a-f\d]{24}$'),))

    class Meta:
        verbose_name = _('Data Meta')
        verbose_name_plural = _('Data Metas')
        ordering = ('-add_time',)

    def delete(self, *args, **kwargs):
        """删除自身，同时删除content，并将所用模板和标签的引用计数 -1 """
        super(DataMeta, self).delete(*args, **kwargs)
        try:
            dc = DataContent.objects.get(pk=self.dc_id)
            dc.delete()
        except DoesNotExist:
            pass
        # fixme: change to celery or use django signal ???
        Template.dec_ref(self.tid)
        for word in self.keywords:
            tag, created = MaterialTag.objects.get_or_create(name=word)
            MaterialTag.dec_count(tag)

    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        # fixme: change to celery or use django signal ???
        if self.pk is None:  # 第一次保存
            Template.inc_ref(self.tid)
            for word in self.keywords:  # 通过 save 来更新的情况
                MaterialTag.add(word)
        super(DataMeta, self).save(force_insert, force_update, using, update_fields)

    def to_dict(self, meta_only=True):
        """返回 meta 部分的 dict"""
        d = {'id': self.id,
             'title': self.title,
             'category': self.category.name_zh,
             'category_id': self.category_id,
             'source': self.source,
             'tid': self.tid,
             'keywords': self.keywords,
             'doi': self.doi,
             'score': self.score,
             'downloads': self.downloads,
             'views': self.views,
             'abstract': self.abstract,
             'purpose': self.purpose,
             'author': self.author.real_name,
             'add_time': timezone.localtime(self.add_time).strftime('%Y-%m-%d %H:%M:%S')}
        if not meta_only:  # 这里最好能将嵌套的数据也都to_dict
            content = DataContent.objects(pk=self.dc_id).first()
            d['content'] = content.to_mongo() if content else None
            # template = Template.objects.get(pk=self.tid)
            # serializer = JSONHandler(mode=DataMode.WRITE, template=template)
            # d['content'] = serializer.data_to_dict(self)
        return d

    def set_source(self, src=None, pro=None, ref=None, others=None):
        source = dict()
        if src:
            source['source'] = src
        if pro:
            source['project'] = pro
        if ref:
            source['reference'] = ref
        if others:
            source['others'] = others
        self.source = source

    @staticmethod
    def add(title: str, category_id: int, tid: int,
            source: dict = dict, keywords: (list, tuple, str) = (), content_id: ObjectId = None,
            doi: str = None, abstract: str = None, purpose: str = None, author=None):
        """务必使用这种方法来添加，**这个并不会存储 content ，反而会生成 content_id**"""
        dm = None
        try:
            dm = DataMeta(title=title, category_id=category_id, tid=tid,
                          doi=doi, abstract=abstract, purpose=purpose)
            dm.dc_id = content_id or ObjectId()
            dm.set_source(source.get('source'), source.get('project'),
                          source.get('reference'), source.get('others'))
            true_keywords = set()
            if keywords:
                if isinstance(keywords, str):
                    keywords = _KEYWORDS_SEP.split(keywords)
                keywords = [w.strip() for w in filter(lambda x: x.strip(), keywords)]
                for word in keywords:
                    true_keywords.add(word)
            dm.keywords = list(true_keywords)
            if author:
                author = User.objects.get(username=author)
                dm.author = author
            dm.save()
            return dm
        except Exception as ex:
            if dm and dm.pk:
                dm.delete()
            raise ex

    @staticmethod
    def add_tags(meta_or_pk, tags):
        """
        添加一个或多个标签（更新到数据库）
        :param meta_or_pk: 待更新的元数据h或元数据 id
        :type meta_or_pk: DataMeta, int
        :param tags: 需添加的标签
        :type tags: list, tuple, str
        :return: None
        """
        try:
            _tags = []
            if isinstance(tags, (list, tuple)):
                _tags = [t.strip() for t in filter(lambda x: x.strip() and x.strip() not in meta_or_pk.keywords, tags)]
            elif isinstance(tags, str):
                tags = tags.strip()
                if tags and tags not in meta_or_pk.keywords:
                    _tags = [tags]
            meta_or_pk.keywords.extend(_tags)
            meta_or_pk.save(update_fields=('keywords',))
            for t in _tags:
                try:
                    tag = MaterialTag.objects.get(name=t)
                    MaterialTag.inc_count(tag)
                except MaterialTag.DoesNotExist:
                    MaterialTag.add(t)
        except Exception as ex:
            logging.error('Some error occur when add tags(%s).\n%s',
                          str(tags), str(ex))
            raise ex

    @staticmethod
    def remove_tags(meta_or_pk, tags):
        """
        删除一个或多个标签多个标签（更新到数据库）
        :param meta_or_pk: 待更新的元数据或元数据 id
        :type meta_or_pk: DataMeta, int
        :param tags: 需添加的标签
        :type tags: list, tuple, str
        :return: None
        """
        try:
            if not isinstance(meta_or_pk, DataMeta):
                meta_or_pk = DataMeta.objects.get(pk=meta_or_pk)
            _tags = set()
            if isinstance(tags, (list, tuple)):
                for t in tags:
                    t = t.strip()
                    if t in meta_or_pk.keywords:
                        meta_or_pk.keywords.remove(t)
                        _tags.add(t)
            elif isinstance(tags, str):
                tags = tags.strip()
                if tags in meta_or_pk.keywords:
                    meta_or_pk.keywords.remove(tags)
                    _tags.add(tags)
            if _tags:
                meta_or_pk.save(update_fields=('keywords',))
            for t in _tags:
                tag = MaterialTag.objects.get_or_create(name=t)
                MaterialTag.dec_count(tag)
        except Exception as ex:
            logging.error('Some error occur when remove tags(%s).\n%s',
                          str(tags), str(ex))
            raise ex

    @staticmethod
    def on_download(meta_pk):
        """
        下载量 +1
        """
        DataMeta.objects.filter(pk=meta_pk).update(downloads=models.F('downloads') + 1)

    @staticmethod
    def on_view(meta_pk):
        """
        访问量 +1
        """
        DataMeta.objects.filter(pk=meta_pk).update(views=models.F('views') + 1)


class DataContent(me.DynamicDocument):
    _meta_id = me.IntField(min_value=0, required=True, db_field='_meta_id')
    _tid = me.IntField(min_value=0, required=True, db_field='_tid')

    meta = {
        'indexes': [
            '_meta_id',
            '_tid',
        ],
    }

    def delete(self, *args, **kwargs):
        """删除同时检查各个字段，删除各字段内容"""
        for field_cls in (DataFieldTable, DataFieldContainer):
            for field in field_cls.objects(_meta_id=self._meta_id, _owner_id=self.pk):
                field.delete()
        super(DataContent, self).delete(*args, **kwargs)

    @staticmethod
    def add(meta_id, tid, content):
        """
        添加新的数据内容并返回
        :param meta_id: 元数据 id
        :param tid: 模板 id
        :param content: 数据内容
        :type content: dict
        :return: 返回新添加的数据内容对象
        :rtype: DataContent
        """
        dc_id = ObjectId()
        try:
            if isinstance(content.get('_id'), ObjectId):
                dc_id = content.pop('_id')
            dc = DataContent(**content)
            dc.id = dc_id
            dc._meta_id = meta_id
            dc._tid = tid
            dc.save()
            return dc
        except ValidationError as ex:
            for field_cls in (DataFieldTable, DataFieldContainer):
                for field in field_cls.objects(_owner_id=dc_id):
                    field.delete()
            raise ex


class DataFieldTable(me.Document):
    """表格型数据

    `title`: 表格标题（table 标签的 title 部分，并非字段名）
    `head`:  列标题（表头）
    """
    _meta_id = me.IntField(min_value=0, required=True, db_field='_meta_id')
    _tid = me.IntField(min_value=0, required=True, db_field='_tid')
    _owner_id = me.ObjectIdField(required=True, db_field='_owner_id')
    _path = me.StringField(required=True, db_field='_path')
    title = me.StringField()
    head = me.ListField(field=me.StringField(required=True))

    meta = {
        'indexes': [
            '_meta_id',
            '_tid',
            '_owner_id',
            '_path',
        ],
    }

    def delete(self, *args, **kwargs):
        """删除同时删除行数据"""
        for row in DataFieldTableRow.objects(_meta_id=self._meta_id, _owner_id=self.pk):
            row.delete()
        super(DataFieldTable, self).delete(*args, **kwargs)

    @staticmethod
    def add(meta_id, tid, owner_id, path, title, head, rows=()):
        """
        创建一个表格数据并返回
        :param meta_id: 元数据 id
        :param tid: 模板 id
        :param owner_id: 所属对象的 ObjectId
        :param path: 对象名称路径
        :param title: 表格标题
        :type title: str
        :param head: 表头名称
        :type head: list
        :param owner_id: 所属对象的 ObjectId
        :type owner_id: str
        :param rows: 二维数组或行对象的一维数组，每一行数据，行内只能是字符串或数值（没有做检验）
        :type head: list
        :return: 返回创建的表格
        :rtype: DataFieldTable
        """
        dt = None
        try:
            dt = DataFieldTable(title=title, head=head, _owner_id=owner_id, _path=path, _meta_id=meta_id, _tid=tid)
            dt.save()
            head_len = len(head)
            for row in rows:
                if isinstance(row, (list, tuple)):
                    assert len(row) == head_len
                    DataFieldTableRow.objects.create(_meta_id=meta_id, _tid=tid, _owner_id=dt.id, _path=path,
                                                     **dict(zip(head, row)))
                elif isinstance(row, dict):
                    DataFieldTableRow.add(meta_id, dt.id, path, row)
                else:
                    raise ValidationError
            return dt
        except Exception as ex:
            if dt and dt.pk:
                dt.delete()
            raise ex

    @staticmethod
    def add_rows(table, rows):
        """
        向`table`添加新的行
        :param table: 待添加行数据的表格
        :type table: DataFieldTable
        :param rows: 待添加的行数据
        :param rows: list
        :return: None
        """
        n_rows = list()
        try:
            head_len = len(table.head)
            for row in rows:
                assert len(row) == head_len
                row = DataFieldTableRow.objects.create(_meta_id=table._meta_id, _tid=table._tid,
                                                       _owner_id=table.id, _path=table._path,
                                                       **dict(zip(table.head, row)))
                n_rows.append(row)
        except ValidationError as ex:
            logging.error('Some error occur when save DataFieldTable(%s).\n%s',
                          table.title, str(ex))
            raise ex
        except Exception as ex:
            for row in n_rows:
                row.delete()
            raise ex


class DataFieldTableRow(me.DynamicDocument):
    """表格字段实际存储的每一行的内容"""

    _meta_id = me.IntField(min_value=0, required=True, db_field='_meta_id')
    _tid = me.IntField(min_value=0, required=True, db_field='_tid')
    _owner_id = me.ObjectIdField(required=True, db_field='_owner_id')
    _path = me.StringField(required=True, db_field='_path')

    meta = {
        'indexes': [
            '_meta_id',
            '_tid',
            '_owner_id',
            '_path',
        ],
    }

    @staticmethod
    def add(meta_id, tid, owner_id, path, row):
        """
        添加一行数据
        :param meta_id: 元数据 id
        :param tid: 模板 id
        :param owner_id: 所属对象的 ObjectId
        :param path: 对象名称路径
        :param row: 需要添加的行，需是表头和值对应的 dict（没有做校验）
        :type row: dict
        :return: 返回新创建的行
        :rtype: DataFieldTableRow
        """
        tr = DataFieldTableRow.objects.create(_meta_id=meta_id, _tid=tid,
                                              _owner_id=ObjectId(owner_id), _path=path,
                                              **row)
        return tr


class DataFieldContainer(me.DynamicDocument):
    """容器型数据，内容不确定"""

    _meta_id = me.IntField(min_value=0, required=True, db_field='_meta_id')
    _tid = me.IntField(min_value=0, required=True, db_field='_tid')
    _owner_id = me.ObjectIdField(required=True, db_field='_owner_id')
    _path = me.StringField(required=True, db_field='_path')

    meta = {
        'indexes': [
            '_meta_id',
            '_tid',
            '_owner_id',
            '_path',
        ],
    }

    def delete(self, *args, **kwargs):
        """删除同时检查各个字段，删除各字段内容"""
        for field_cls in (DataFieldTable, DataFieldContainer):
            for field in field_cls.objects(_meta_id=self._meta_id, _owner_id=self.pk):
                field.delete()
        super(DataFieldContainer, self).delete(*args, **kwargs)

    @staticmethod
    def add(meta_id, tid, owner_id, path, content: dict):
        """
        添加新的容器型数据并返回
        :param meta_id: 元数据 id
        :param tid: 模板 id
        :param owner_id: 所属对象的 ObjectId
        :param path: 对象名称路径
        :param content: 待添加的容器型数据
        :type content: dict
        :return: 返回添加的容器型数据
        :rtype: DataFieldContainer
        """
        ctn_id = ObjectId()
        try:
            if isinstance(content.get('_id'), ObjectId):
                ctn_id = content.pop('_id')
            dfc = DataFieldContainer.objects.create(id=ctn_id, _meta_id=meta_id, _tid=tid, _owner_id=owner_id,
                                                    _path=path,
                                                    **content)
            return dfc
        except ValidationError as ex:
            for field_cls in (DataFieldTable, DataFieldContainer):
                for field in field_cls.objects(_owner_id=ctn_id):
                    field.delete()
            raise ex


class SearchRecord(models.Model):
    author = models.ForeignKey(to='accounts.User', null=True, on_delete=models.SET_NULL)
    record = models.CharField(max_length=255)
    search_time = models.DateTimeField(default=timezone.now)


# lyp新加的

from django.db import transaction


class DataUploadSourceMap(models.Model):
    meta = models.OneToOneField(DataMeta, on_delete=models.CASCADE)
    file = models.ForeignKey(UploadedSourceFile, on_delete=models.SET_NULL, null=True)


class DataReviewState(IntEnum):
    PENDING = 0
    APPROVED = 1
    DISAPPROVED = 2

    @property
    def description(self):
        if self == DataReviewState.PENDING:
            return ugettext("Reviewing Pending")
        elif self == DataReviewState.APPROVED:
            return ugettext("Approved")
        elif self == DataReviewState.DISAPPROVED:
            return ugettext("Disapproved")


class DisapproveReason(IntEnum):
    OTHER = 0
    META_MISSING = 1
    CONDITION_MISSING = 2
    PERFORMANCE_INFO_MISSING = 3
    COMPONENTS_MISSING = 4
    INFORMAL_TITLE = 5
    REFERENCE_MISSING = 6
    CONTRIBUTOR_MISSING = 7

    @property
    def to_string(self):
        if self == 0:
            return _("Other")
        elif self == 1:
            return _("Missing some meta fields")
        elif self == 2:
            return _("Missing conditions of computation/experiment")
        elif self == 3:
            return _("Missing conditions of computation/experiment")
        elif self == 4:
            return _("Missing information of performance")
        elif self == 5:
            return _("Title lacks standardization")
        elif self == 6:
            return _("Missing references")
        elif self == 7:
            return _("Missing collector/contributor/reviewer")


class UploadHistory(models.Model):
    class Meta:
        ordering = ("-time",)

    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='upload_histories')
    time = models.DateTimeField(auto_now_add=True)
    meta_id_list = ArrayField(models.IntegerField())
    count = models.IntegerField()
    source = models.ForeignKey(UploadedSourceFile, on_delete=models.SET_NULL, null=True)
    # 判断时统一以source为准，source为空表示表单提交，否则是文件提交
    via_file = models.BooleanField(default=False)
    review_state = models.IntegerField(default=DataReviewState.PENDING)
    reviewer = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    category = models.ForeignKey(MaterialCategory, null=True, on_delete=models.SET_NULL)
    disapprove_reason = models.TextField(null=True)

    @property
    def reason_list(self):
        if self.disapprove_reason:
            return [ugettext(x) for x in self.disapprove_reason.split(';')]
        else:
            return []

    @property
    def source_url(self):
        if self.source:
            return self.source.get_url(absolute=True)

    def delete(self, using=None, keep_parents=False):
        if self.source:
            self.source.delete()
        super().delete(using, keep_parents)

    @property
    def review_status_description(self):
        return DataReviewState(self.review_state).description

    @property
    def pending(self):
        return self.review_state == DataReviewState.PENDING

    @property
    def approved(self):
        return self.review_state == DataReviewState.APPROVED

    @property
    def disapproved(self):
        return self.review_state == DataReviewState.DISAPPROVED

    @property
    def reviewer_real_name(self):
        return self.reviewer.real_name if self.reviewer else _("User Deleted")

    @property
    def uploaded_by_real_name(self):
        return self.user.real_name if self.user else _("User Deleted")

    def approve_data(self, reviewer: User, approved, reasons=None, reason=None):
        state = DataReviewState.APPROVED if approved else DataReviewState.DISAPPROVED
        update_kwargs = dict(review_state=state, reviewer=reviewer)
        if not approved:
            if not reasons and not reason:
                raise ValueError("Must provide at least one reason")
            if reasons is not None:
                reasons = [DisapproveReason(x).to_string for x in reasons]
                update_kwargs['disapprove_reason'] = ';'.join(reasons)
            else:
                update_kwargs['disapprove_reason'] = reason
        with transaction.atomic():
            DataMeta.objects.filter(id__in=self.meta_id_list).update(**update_kwargs)
            UploadHistory.objects.filter(id=self.id).update(**update_kwargs)
